/*
 *
 * The DbUnit Database Testing Framework
 * Copyright (C)2002-2004, DbUnit.org
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 */
package org.dbunit.dataset;

import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.SQLException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import org.dbunit.DatabaseUnitRuntimeException;
import org.dbunit.database.DatabaseConfig;
import org.dbunit.database.IDatabaseConnection;
import org.dbunit.dataset.datatype.IDataTypeFactory;
import org.dbunit.dataset.datatype.IDbProductRelatable;
import org.dbunit.dataset.filter.IColumnFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * @author Manuel Laflamme
 * @author Last changed by: $Author: gommma $
 * @version $Revision: 1031 $ $Date: 2009-09-12 17:13:43 +0200 (sab, 12 set 2009) $
 * @since 1.0 (Mar 8, 2002)
 */
public abstract class AbstractTableMetaData implements ITableMetaData {

    private Map _columnsToIndexes;

    /**
     * Logger for this class
     */
    private static final Logger logger = LoggerFactory.getLogger(AbstractTableMetaData.class);

    /**
     * Default constructor
     */
    public AbstractTableMetaData() {
    }

    /**
     * @param columns
     * @param keyNames
     * @return The primary key columns
     * @deprecated since 2.3.0 - use {@link Columns#getColumns(String[], Column[])}
     */
    protected static Column[] getPrimaryKeys(Column[] columns, String[] keyNames) {
        logger.debug("getPrimaryKeys(columns={}, keyNames={}) - start", columns, keyNames);
        return Columns.getColumns(keyNames, columns);
    }

    /**
     * @param tableName
     * @param columns
     * @param columnFilter
     * @return The filtered primary key columns
     * @deprecated since 2.3.0 - use {@link Columns#getColumns(String[], Column[])}
     */
    protected static Column[] getPrimaryKeys(String tableName, Column[] columns,
                                             IColumnFilter columnFilter) {
        if (logger.isDebugEnabled()) {
            logger.debug("getPrimaryKeys(tableName={}, columns={}, columnFilter={}) - start",
                    new Object[]{tableName, columns, columnFilter});
        }
        return Columns.getColumns(tableName, columns, columnFilter);
    }

    /**
     * Provides the index of the column with the given name within this table.
     * Uses method {@link ITableMetaData#getColumns()} to retrieve all available columns.
     *
     * @throws DataSetException
     * @see org.dbunit.dataset.ITableMetaData#getColumnIndex(String)
     */
    public int getColumnIndex(String columnName) throws DataSetException {
        logger.debug("getColumnIndex(columnName={}) - start", columnName);

        if (this._columnsToIndexes == null) {
            // lazily create the map
            this._columnsToIndexes = createColumnIndexesMap(this.getColumns());
        }

        String columnNameUpperCase = columnName.toUpperCase();
        Integer colIndex = (Integer) this._columnsToIndexes.get(columnNameUpperCase);
        if (colIndex != null) {
            return colIndex.intValue();
        } else {
            throw new NoSuchColumnException(" (Non-uppercase input column: " + columnName + ") in ColumnNameToIndexes cache map. " +
                    "Note that the map's column names are NOT case sensitive.");
        }
    }

    /**
     * @param columns The columns to be put into the hash table
     * @return A map having the key value pair [columnName, columnIndexInInputArray]
     */
    private Map createColumnIndexesMap(Column[] columns) {
        Map colsToIndexes = new HashMap(columns.length);
        for (int i = 0; i < columns.length; i++) {
            colsToIndexes.put(columns[i].getColumnName().toUpperCase(), new Integer(i));
        }
        return colsToIndexes;
    }

    /**
     * Validates and returns the datatype factory of the given connection
     *
     * @param connection The connection providing the {@link IDataTypeFactory}
     * @return The datatype factory of the given connection
     * @throws java.sql.SQLException
     */
    public IDataTypeFactory getDataTypeFactory(IDatabaseConnection connection)
            throws SQLException {
        DatabaseConfig config = connection.getConfig();
        Object factoryObj = config.getProperty(DatabaseConfig.PROPERTY_DATATYPE_FACTORY);
        if (!IDataTypeFactory.class.isAssignableFrom(factoryObj.getClass())) {
            String msg = "Invalid datatype factory configured. Class '" +
                    factoryObj.getClass() + "' does not implement '" + IDataTypeFactory.class + "'.";
            if (factoryObj instanceof String) {
                msg += " Ensure not to specify the fully qualified class name as String but the concrete " +
                        "instance of the datatype factory (for example 'new OracleDataTypeFactory()').";
            }
            // TODO Would a "DatabaseUnitConfigurationException make more sense?
            throw new DatabaseUnitRuntimeException(msg);
        }
        IDataTypeFactory dataTypeFactory = (IDataTypeFactory) factoryObj;

        // Validate, e.g. oracle metaData + oracleDataTypeFactory ==> OK
        Connection jdbcConnection = connection.getConnection();
        DatabaseMetaData metaData = jdbcConnection.getMetaData();
        String validationMessage = validateDataTypeFactory(dataTypeFactory, metaData);
        if (validationMessage != null) {
            // Inform the user that we think he could get trouble with the current configuration
            logger.warn("Potential problem found: " + validationMessage);
        }

        return dataTypeFactory;
    }

    /**
     * Verifies that the data type factory supports the database product on the connection.
     * If the data type factory is not valid for the connection, a warning is logged.
     *
     * @param dataTypeFactory The data type factory to validate.
     * @param metaData        The {@link java.sql.DatabaseMetaData} needed to get the DB product name of the connection RDBMS.
     * @return A validation message if there is a potential problem or <code>null</code> if everything is fine.
     * @throws java.sql.SQLException A database problem.
     */
    String validateDataTypeFactory(IDataTypeFactory dataTypeFactory, DatabaseMetaData metaData)
            throws SQLException {
        if (!(dataTypeFactory instanceof IDbProductRelatable)) {
            return null;
        }
        IDbProductRelatable productRelatable = (IDbProductRelatable) dataTypeFactory;
        String databaseProductName = metaData.getDatabaseProductName();

        Collection validDbProductCollection = productRelatable.getValidDbProducts();
        if (validDbProductCollection != null) {
            String lowerCaseDbProductName = databaseProductName.toLowerCase();
            for (Iterator iterator = validDbProductCollection.iterator(); iterator.hasNext(); ) {
                String validDbProduct = ((String) iterator.next()).toLowerCase();
                if (lowerCaseDbProductName.indexOf(validDbProduct) > -1) {
                    logger.debug("The current database '{}' fits to the configured data type factory '{}'. Validation successful.",
                            databaseProductName, dataTypeFactory);
                    return null;
                }
            }
        }

        // If we get here, the validation failed
        String validationMessage = "The configured data type factory '" + dataTypeFactory.getClass() +
                "' might cause problems with the current database '" + databaseProductName +
                "' (e.g. some datatypes may not be supported properly). " +
                "In rare cases you might see this message because the list of supported database " +
                "products is incomplete (list=" + validDbProductCollection + "). " +
                "If so please request a java-class update via the forums." +
                "If you are using your own IDataTypeFactory extending " +
                "DefaultDataTypeFactory, ensure that you override getValidDbProducts() " +
                "to specify the supported database products.";
        return validationMessage;
    }
}
